package com.qianxun.framework.util.http;

import javax.net.ssl.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * http请求连接处理对象
 *
 * @author huwei 3313749341@qq.com
 */
public final class HttpConnection {
    // 默认编码
    private static final String ENCODE = "UTF-8";
    // 请求体,存储设置请求参数
    private HttpRequest httpRequest;
    //上传文件
    private AtomicBoolean uploadFile = new AtomicBoolean(false);
    //上传文件信息
    private List<UploadFileInfo> uploadFileInfoList;

    public HttpConnection(String url) {
        this.httpRequest = new HttpRequest(url);
    }

    public HttpConnection setRequestMethod(RequestMethod method) {
        httpRequest.setMethod(method);
        return this;
    }

    public HttpConnection setConnectionTimeout(Integer connectionTimeout) {
        httpRequest.setConnectionTimeout(connectionTimeout);
        return this;
    }

    /**
     * 设置接受响应超时时间
     *
     * @param readTimeout 响应超时时间
     * @return
     */
    public HttpConnection setReadTimeout(Integer readTimeout) {
        httpRequest.setReadTimeout(readTimeout);
        return this;
    }

    /**
     * 设置是否允许自动重定向
     *
     * @param isRedirect 是否允许自动重定向
     * @return
     */
    public HttpConnection setRedirect(Boolean isRedirect) {
        httpRequest.setRedirect(isRedirect);
        return this;
    }

    public HttpConnection setUseCache(Boolean useCache) {
        httpRequest.setUseCache(useCache);
        return this;
    }

    /**
     * 设置请求头,该方法将覆盖同名属性的值
     *
     * @param header
     * @param value
     * @return
     */
    public HttpConnection setHeader(String header, String value) {
        setHeader(header, Arrays.asList(value));
        return this;
    }

    /**
     * 设置请求头,该方法将覆盖同名属性的值
     *
     * @param header
     * @param value
     * @return
     */
    public HttpConnection setHeader(String header, List<String> value) {
        httpRequest.getHeaders().put(header, value);
        return this;
    }

    /**
     * 设置请求头,该方法将覆盖同名属性的值
     *
     * @param headers 请求属性的map
     * @return
     */
    public HttpConnection setHeader(Map<String, String> headers) {
        if (headers != null) {
            headers.forEach((k, v) -> {
                setHeader(k, v);
            });
        }
        return this;
    }

    /**
     * 设置请求头,该方法将覆盖同名属性的值
     *
     * @param headers 请求属性的map
     * @return
     */
    public HttpConnection setHeaderMore(Map<String, List<String>> headers) {
        if (headers != null) {
            headers.forEach((k, v) -> {
                setHeader(k, v);
            });
        }
        return this;
    }

    /**
     * 设置请求头,该方法不会覆盖现有的同名属性的值,会追加到该属性后面
     *
     * @param header 属性
     * @param value  值
     * @return
     */
    public HttpConnection addHeader(String header, String value) {
        Map<String, List<String>> headers = httpRequest.getHeaders();
        List<String> old = headers.get(header);
        if (old == null) {
            old = new ArrayList<String>(16);
        }
        old.add(value);
        headers.put(header, old);
        return this;
    }

    /**
     * 添加请求参数,该方法将为该请求添加多个相同名称参数,其格式类似于:&a=1&a=2&a=3
     *
     * @param parameter 参数名
     * @param value     参数值
     * @return
     */
    public HttpConnection addParameter(String parameter, String value) {
        Map<String, List<String>> paramters = httpRequest.getParameters();
        List<String> old = paramters.get(parameter);
        if (old == null) {
            old = new ArrayList<String>(16);
        }
        old.add(value);
        paramters.put(parameter, old);
        return this;
    }

    /**
     * 用于为post请求方式设置参数 注意:当使用此方式设置参数后,使用addParameter或setParameter方法设置的参数将不被生效;
     *
     * @param content
     * @return
     */
    public HttpConnection setPostParameter(byte[] content) {
        httpRequest.setParameterConent(content);
        return this;
    }

    /**
     * 设置请求参数,该方法将覆盖现有的参数值
     *
     * @param parameter 参数名
     * @param value     参数值
     * @return
     */
    public HttpConnection setParameter(String parameter, String value) {
        setParameter(parameter, Arrays.asList(value));
        return this;
    }

    /**
     * 设置请求参数,该方法将覆盖现有的参数值
     *
     * @param parameter 参数名
     * @param value     参数值
     * @return
     */
    public HttpConnection setParameter(String parameter, List<String> value) {
        httpRequest.getParameters().put(parameter, value);
        return this;
    }

    /**
     * 设置请求参数,该方法将覆盖现有的参数值
     *
     * @return
     */
    public HttpConnection setParameter(Map<String, String> parameters) {
        if (parameters != null) {
            parameters.forEach((k, v) -> {
                setParameter(k, v);
            });
        }
        return this;
    }

    /**
     * 设置请求参数,该方法将覆盖现有的参数值
     *
     * @return
     */
    public HttpConnection setParameterMore(Map<String, List<String>> parameters) {
        if (parameters != null) {
            parameters.forEach((k, v) -> {
                setParameter(k, v);
            });
        }
        return this;
    }

    public HttpConnection setEncode(String encode) {
        httpRequest.setEncode(encode);
        return this;
    }

    public HttpResponse execute() throws IOException {
        checkUrlLegal();
        // https请求专属参数设置
        if (isHttps()) {
            setHttpsParameters();
        }
        String url = httpRequest.getUrl();
        // get参数设置
        if (RequestMethod.GET.name().equals(getMethod())) {
            // 设置请求参数
            url = setRequestParametersForMethodGet(url);
        }
        // 创建并打开链接
        URL urlObj = new URL(encodeUrl(url));
        HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection();
        // 属性设置
        setHeaders(connection);
        if (!RequestMethod.GET.name().equals(getMethod())) {
            // 设置请求方式,默认为get
            connection.setRequestMethod(getMethod());
            // 设置请求参数
            setRequestParametersForMethodPost(connection);
        }
        // 建立连接
        connection.connect();
        // 获取响应信息
        byte[] response = readAndDisconnect(connection);

        //清除当次请求信息
        clear();
        return new HttpResponse(connection, response);
    }

    public HttpResponse get() throws IOException {
        httpRequest.setMethod(RequestMethod.GET);
        return execute();
    }

    public HttpResponse post() throws IOException {
        httpRequest.setMethod(RequestMethod.POST);
        return execute();
    }

    public HttpResponse upload(UploadFileInfo uploadFileInfo) throws IOException {
        if (uploadFileInfo == null) {
            throw new IOException("上传文件不能为空");
        }
        return upload(Arrays.asList(uploadFileInfo));
    }

    public HttpResponse upload(List<UploadFileInfo> uploadFileInfoList) throws IOException {
        if (uploadFileInfoList == null || uploadFileInfoList.isEmpty()) {
            throw new IOException("上传文件不能为空");
        }
        httpRequest.setMethod(RequestMethod.POST);
        uploadFile.set(true);
        this.uploadFileInfoList = uploadFileInfoList;
        return execute();
    }

    private String getMethod() {
        return httpRequest.getMethod() == null ? RequestMethod.GET.name() : httpRequest.getMethod().name();
    }

    private boolean isHttps() {
        return httpRequest.getUrl() != null && httpRequest.getUrl().startsWith("https");
    }

    private boolean isHttp() {
        return httpRequest.getUrl() != null && httpRequest.getUrl().startsWith("http");
    }

    private String getEncode() {
        return httpRequest.getEncode() == null ? ENCODE : httpRequest.getEncode();
    }

    /**
     * 处理基于https协议请求的ssl认证问题,默认采用空实现,不做任何处理
     */
    private void setHttpsParameters() {
        SSLContext sslcontext = null;
        try {
            sslcontext = SSLContext.getInstance("SSL", "SunJSSE");
            try {
                sslcontext.init(null, new TrustManager[]{new NUllX509TrustManager()},
                        new java.security.SecureRandom());
            } catch (KeyManagementException e) {
                // TODO Auto-generated catch block
            }
            HostnameVerifier ignoreHostnameVerifier = new HostnameVerifier() {
                public boolean verify(String s, SSLSession sslsession) {
                    System.out.println("WARNING: Hostname is not matched for cert.");
                    return true;
                }
            };
            HttpsURLConnection.setDefaultHostnameVerifier(ignoreHostnameVerifier);
            HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory());
        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
            // TODO Auto-generated catch block
        }
    }

    private static String encodeUrl(String url) {
        try {
            URL u = new URL(url);
            return encodeUrl(u).toExternalForm();
        } catch (Exception e) {
            return url;
        }
    }

    private static URL encodeUrl(URL u) {
        try {
            String urlS = u.toExternalForm();
            urlS = urlS.replaceAll(" ", "%20");
            final URI uri = new URI(urlS);
            return new URL(uri.toASCIIString());
        } catch (Exception e) {
            return u;
        }
    }

    private static byte[] readAndDisconnect(HttpURLConnection connection) throws IOException {
        connection.connect();
        ByteArrayOutputStream res = new ByteArrayOutputStream();
        InputStream is = connection.getInputStream();
        int len;
        byte[] buf = new byte[1024];
        while ((len = is.read(buf, 0, buf.length)) != -1) {
            res.write(buf, 0, len);
        }
        is.close();
        res.close();
        connection.disconnect();
        return res.toByteArray();
    }

    private void setHeaders(HttpURLConnection connection) {
        // 连接超时设置
        if (httpRequest.getConnectionTimeout() != null) {
            connection.setConnectTimeout(httpRequest.getConnectionTimeout());
        }
        // 读取超时设置
        if (httpRequest.getReadTimeout() != null) {
            connection.setReadTimeout(httpRequest.getReadTimeout());
        }
        // 重定向设置
        if (httpRequest.getRedirect() != null) {
            connection.setInstanceFollowRedirects(httpRequest.getRedirect());
        }
        // 缓存设置
        if (httpRequest.getUseCache() != null) {
            connection.setUseCaches(httpRequest.getUseCache());
        }
        // 请求头的属性设置
        httpRequest.getHeaders().forEach((k, v) -> {
            StringBuffer sb = new StringBuffer();
            v.forEach((e) -> {
                sb.append(",").append(e);
            });
            String value = sb.length() > 0 ? sb.deleteCharAt(0).toString() : "";
            connection.setRequestProperty(k, value);
        });
    }

    private void setRequestParametersForMethodPost(HttpURLConnection connection) throws IOException {
        byte[] content = null;
        if (httpRequest.getParameterConent() != null) {
            content = httpRequest.getParameterConent();
        } else if (!uploadFile.get()) {
            content = wrapRequestParameter().getBytes(getEncode());
        } else {
            //文件上传，参数封装
            content = wrapUploadParameter(connection);
        }
        connection.setRequestProperty("Content-Length", String.valueOf(content.length));
        connection.setDoOutput(true);
        DataOutputStream out = new DataOutputStream(connection.getOutputStream());
        out.write(content);
        out.flush();
        out.close();

        int resultCode = connection.getResponseCode();
    }

    private byte[] wrapUploadParameter(HttpURLConnection connection) {
        final String br = "\r\n";
        final String requestType = "Content-Disposition: form-data; ";
        final String randomSign = String.valueOf(System.currentTimeMillis());
        final String startPrefix = "------".concat(randomSign).concat(br);
        final String endPrefix = "------".concat(randomSign).concat("--");

        //set head
        connection.setRequestProperty("Connection", "Keep-Alive");
        connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=".concat("----").concat(randomSign));

        StringBuffer parameterBuf = new StringBuffer();
        httpRequest.getParameters().forEach((k, v) -> {
            v.forEach((e) -> {
                //start
                parameterBuf.append(startPrefix);
                //content head
                parameterBuf.append(requestType).append("name=").append("\"").append(k).append("\"").append(br);
                //content
                parameterBuf.append(e).append(br);
            });
        });
        byte[] body = getByteCode(parameterBuf.toString());

        for (int i = 0; i < uploadFileInfoList.size(); ++i) {
            UploadFileInfo uploadFileInfo = uploadFileInfoList.get(i);
            StringBuffer fileParameter = new StringBuffer();
            //start
            fileParameter.append(startPrefix);
            //content head
            fileParameter.append(requestType).append("name=").append("\"").append(uploadFileInfo.getUploadParameterName()).append("\"").append("; ")
                    .append("filename=").append("\"").append(uploadFileInfo.getFileName()).append("\"").append(br);
            //content type
            fileParameter.append("Content-Type: ").append(uploadFileInfo.getContentType()).append(br).append(br);
            //content
            body = contact(body, getByteCode(fileParameter.toString()));
            body = contact(body, uploadFileInfo.getFile());

            //换行
            body = contact(body, getByteCode(br));
        }
        //tail
        body = contact(body, endPrefix.getBytes());
        return body;
    }

    private byte[] contact(byte[] a, byte[] b) {
        byte[] c = new byte[a.length + b.length];
        System.arraycopy(a, 0, c, 0, a.length);
        System.arraycopy(b, 0, c, a.length, b.length);
        return c;
    }

    private byte[] getByteCode(String string) {
        try {
            return string.getBytes(getEncode());
        } catch (UnsupportedEncodingException e) {
            return string.getBytes();
        }
    }

    private String setRequestParametersForMethodGet(String url) {
        return url += new StringBuffer("?").append(wrapRequestParameter()).toString();
    }

    private String wrapRequestParameter() {
        StringBuffer sb = new StringBuffer();
        httpRequest.getParameters().forEach((k, v) -> {
            String encodeKey;
            try {
                 encodeKey = URLEncoder.encode(k, getEncode());
            }catch (UnsupportedEncodingException ex) {
                throw new RuntimeException("编码异常：".concat(getEncode()) ,ex);
            }
            v.forEach((e) -> {
                String encodeValue;
                try {
                    encodeValue = URLEncoder.encode(e ,getEncode());
                } catch (UnsupportedEncodingException ex) {
                    throw new RuntimeException("编码异常：".concat(getEncode()) ,ex);
                }
                sb.append("&").append(encodeKey).append("=").append(encodeValue);
            });
        });

        String result = sb.length() == 0 ? "" : sb.deleteCharAt(0).toString();

        return result;
    }

    private void checkUrlLegal() {
        if (!(isHttps() || isHttp())) {
            throw new RuntimeException("url illegal ,the url be start with http or https!");
        }
    }

    private void clear() {
        httpRequest = null;
        uploadFileInfoList = null;
        uploadFile.set(false);
    }

    /**
     * X509TrustManager接口的空实现,未做任何处理 用于处理客户提交的基于https协议的ssl请求,
     *
     * @author huwei
     */
    private static class NUllX509TrustManager implements X509TrustManager {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        }

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    }
}